from panda3d.core import Vec2, Point3, Vec3, Vec4, BitMask32
from direct.interval.IntervalGlobal import *
import math
from pandac.PandaModules import CollisionSphere, CollisionNode, CollisionHandlerPusher, CollisionTraverser, CollisionHandlerQueue
from pandac.PandaModules import TextureStage

def distance(pos1, pos2):
	diff = pos1 - pos2
	return math.sqrt(diff[0]**2 + diff[1]**2 + diff[2]**2)
	
def length(pos):
	return math.sqrt(pos[0]**2 + pos[1]**2 + pos[2]**2)

def length2D(pos):
	return math.sqrt(pos[0]**2 + pos[1]**2)

'''
	Andrew Oberlin
	Date: March 6, 2012
'''
class VisualObject3D:
	'''
		This object will extended for buildings and other objects
		that are placed within the scene
		
		It represents the minerals in the game
	'''
	def __init__(self, parent, dummy, key):
		from object_const import ObjectConstants
		self.parent = parent
		self.dummy = dummy
		self.const = ObjectConstants()
		self.type = self.const.MINERALS
		self.height = 3
		self.textureFile = "models/terrain/minerals.png"
		self.model = self.loadModel()
		self.model.setScale(5)
		self.model.setP(90)
		self.key = key
		self.model.setTag('key', key)
		self.health = 1000
		self.originalHealth = 1000
		self.alive = True
		
		self.collisionSolid = CollisionSphere(0, 0, -0.3, 0.75)
		self.collisionNode = CollisionNode('cnode_' + self.key)
		self.cnodePath = self.model.attachNewNode(self.collisionNode)
		self.cnodePath.node().addSolid(self.collisionSolid)
		self.collisionNode.setIntoCollideMask(BitMask32(0x02))
		
	'''
		Loads a model correctly
	'''
	def loadModel(self):

		model = self.parent.loader.loadModelCopy("models/plane")
		texture = self.parent.loader.loadTexture(self.textureFile)
		model.setTexture(texture)
		model.setTransparency(1)
		model.reparentTo(self.dummy)
		
		return model	
		
	'''
	
	'''
	def hide(self):
		self.model.hide()
		
	'''
	
	'''
	def show(self):
		self.model.show()
	
	'''
		Wrapper for the model's position method
	'''
	def setPos(self, pos):
		self.model.setPos(pos)
		
	'''
		Wrapper for the model's position method
	'''
	def setX(self, x):
		self.model.setX(x)
		
	'''
		Wrapper for the model's position method
	'''
	def setY(self, y):
		self.model.setY(y)
		
	'''
		Wrapper for the model's position method
	'''
	def setZ(self, z):
		self.model.setZ(z)
		
		
	'''
		Wrapper for the model's position method
	'''
	def getPos(self):
		return self.model.getPos()
		
	'''
		Wrapper for the model's position method
	'''
	def getX(self):
		return self.model.getX()
		
	'''
		Wrapper for the model's position method
	'''
	def getY(self):
		return self.model.getY()
		
		
	'''
		Wrapper for the model's position method
	'''
	def getZ(self):
		return self.model.getZ()
	
	'''
		Wrapper for the model's position method
	'''
	def setHpr(self, hpr):
		self.model.setHpr(hpr)
		
	'''
		Wrapper for the model's position method
	'''
	def setH(self, h):
		self.model.setH(h)
		
	'''
		Wrapper for the model's position method
	'''
	def setP(self, p):
		self.model.setP(p)
		
	'''
		Wrapper for the model's position method
	'''
	def setR(self, r):
		self.model.setR(r)
		
		
	'''
		Wrapper for the model's hpr method
	'''
	def getHpr(self):
		return self.model.getHpr()
		
	'''
		Wrapper for the model's heading method
	'''
	def getH(self):
		return self.model.getH()
		
	'''
		Wrapper for the model's pitch method
	'''
	def getP(self):
		return self.model.getP()
		
	'''
		Wrapper for the model's roll method
	'''
	def getR(self):
		return self.model.getR()	
		
	'''
		Removes the model node
	'''
	def removeNode(self):
		self.model.removeNode()
		
		
	'''
		Gets the texture for the model node
	'''
	def getTexture(self):
		return self.model.getTexture()
	
	'''
	
	'''
	def setColor(self, r, g, b, a):
		self.model.setColorScale(r, g, b, a)
	
	'''
		Sets the key of the collision node and this
		object in general
	'''
	def setKey(self, key):
		self.key = key
		if (self.collisionNode):
			self.collisionNode.setName(key)
		self.model.setTag('key', key)
		
	'''
		Makes this character lose health points
	'''
	def loseHealth(self, hitPoints, attacker):
		self.health -= hitPoints
		if (self.health <= 0):
			self.alive = False
			if (self.dieAnimation):	
				self.dieAnimation()
	
	def setGameStats(self, singleplayer):
		self.gameStats = singleplayer.gameStats
		self.singleplayer = singleplayer
	
	'''
		Makes this character die
	'''
	def dieAnimation(self):
		self.removeNode()
		self.singleplayer.removeItem(self.key)
	
class Vespene(VisualObject3D):
	'''
		This object will extended for buildings and other objects
		that are placed within the scene
		
		It represents the minerals in the game
	'''
	def __init__(self, parent, dummy, key):
		from object_const import ObjectConstants
		self.parent = parent
		self.dummy = dummy
		self.const = ObjectConstants()
		self.type = self.const.VESPENE
		self.height = 2
		self.textureFile = "models/terrain/vespene.png"
		self.model = self.loadModel()
		self.model.setScale(Vec3(5,1,3))
		self.model.setP(90)
		self.key = key
		self.model.setTag('key', key)
		self.health = 1000
		self.originalHealth = 2500
		self.alive = True
		
		self.collisionSolid = CollisionSphere(0, 0, -0.375, 0.75)
		self.collisionNode = CollisionNode('cnode_' + self.key)
		self.cnodePath = self.model.attachNewNode(self.collisionNode)
		self.cnodePath.node().addSolid(self.collisionSolid)
		self.collisionNode.setIntoCollideMask(BitMask32(0x02))
		
class CommandCenter(VisualObject3D):
	'''
		This object will extended for buildings and other objects
		that are placed within the scene
		
		It represents the minerals in the game
	'''
	def __init__(self, parent, dummy, key):
		from object_const import ObjectConstants
		self.const = ObjectConstants()
		self.textureFile = "models/terran/buildings/command_center.png"
		
		self.super(parent, dummy, key, [400, 0], 20, self.const.SCV, self.const.COMMAND_CENTER, 8, 8, Vec3(0, 0, -0.3), 0.75, 500)
	
	def super(self, parent, dummy, key, price, buildTime, unit, type, height, scale, collPos, collRadius, health):
		self.parent = parent
		self.dummy = dummy
		self.price = price
		self.buildTime = buildTime
		self.unit = unit
		self.type = type
		self.height = height
		self.health = health
		self.originalHealth = health
		self.model = self.loadModel()
		self.model.setScale(scale)
		self.model.setP(90)
		self.key = key
		self.clickable = True
		self.model.setTag('key', self.key)
		self.gameStats = None
		self.alive = True
		
		self.queue = [None for i in range(5)]
		self.lastIndex = 0
		
		self.collisionSolid = CollisionSphere(collPos[0], collPos[1], collPos[2], collRadius)
		self.collisionNode = CollisionNode('cnode_' + self.key)
		self.cnodePath = self.model.attachNewNode(self.collisionNode)
		self.cnodePath.setColorScale(0.5, 0.5, 0.5, 0.1)
		self.cnodePath.node().addSolid(self.collisionSolid)
		self.collisionNode.setIntoCollideMask(BitMask32(0x02))
		
		task = self.parent.taskMgr.add(self.unitCreationTask, self.key + '_creation')
		task.prevTime = 0
	
	def getPrice(self):
		return self.price
	
	def getButtons(self):
		return [
			[
				'create_scv', '', '', 
				'', '', '',
				'', '', 'cancel'
			],
			[
				(self.createUnit, []), '', '', 
				'', '', '',
				'', '', (self.cancelLast, [])
			]
		]
		
	def createUnit(self):
		if (self.gameStats):
			if (self.lastIndex < 4):
				if (self.gameStats.purchase(50, 0, 1)):
					self.queue[self.lastIndex + 1] = -1
					self.lastIndex += 1
	
	def cancelLast(self):
		if (self.gameStats):
			if (self.lastIndex > 0):
				self.gameStats.addMinerals(50)
				self.gameStats.addUnitsUsed(-1)
				self.queue[self.lastIndex] = None
				self.lastIndex -= 1
				
	def unitCreationTask(self, task):
		deltaT = task.time - task.prevTime
		task.prevTime = task.time
		prev_expiry = 0
		for (counter, unit_expiry) in enumerate(self.queue):
			if (unit_expiry):
				# the timer is already ticking on this one
				if (unit_expiry > 0):
					prev_expiry = unit_expiry
					unit_expiry -= deltaT
					self.queue[counter] = unit_expiry
					if (unit_expiry <= 0):
						self.queue[counter] = None
						self.lastIndex -= 1
						self.singleplayer.count += 1
						key = 'unit_' + str(self.singleplayer.count)
						self.singleplayer.units[key] = self.const.getObjectFromNo(
							self.unit, self.parent, self.dummy, key
						)
						self.singleplayer.units[key].addCollisionDetection(self.singleplayer.collisionTraverser)
						self.singleplayer.units[key].setX(self.getX() - 5)
						self.singleplayer.units[key].setY(-self.height/2)
						self.singleplayer.units[key].setZ(self.getZ() - 3)
						if (self.unit == self.const.SCV):
							self.singleplayer.units[key].setCommandCenter(self)
						if (self.unit == self.const.MARINE):
							self.parent.soundLoader.playMusic('create_marine')
				# this one needs to have its timer set		
				elif (unit_expiry <= 0):
					unit_expiry = prev_expiry + 5
					self.queue[counter] = unit_expiry
					prev_expiry = unit_expiry
					
		return task.cont
		
	def allowClick(self, clickable):
		self.clickable = clickable
	
class Barracks(CommandCenter):
	'''
		This object will extended for buildings and other objects
		that are placed within the scene
		
		It represents the minerals in the game
	'''
	def __init__(self, parent, dummy, key):
		from object_const import ObjectConstants
		self.const = ObjectConstants()
		self.textureFile = "models/terran/buildings/barracks.png"
		
		self.super(parent, dummy, key, [150, 0], 15, self.const.MARINE, self.const.BARRACKS, 7, Vec3(8,9,8), Vec3(0, 0, -0.3), 0.75, 200)
	

class InfestedCommandCenter(CommandCenter):
	'''
		This object will extended for buildings and other objects
		that are placed within the scene
		
		It represents the minerals in the game
	'''
	def __init__(self, parent, dummy, key):
		from object_const import ObjectConstants
		self.parent = parent
		self.dummy = dummy
		self.const = ObjectConstants()
		self.type = self.const.INFESTED_COMMAND_CENTER
		self.height = 8
		self.textureFile = "models/zerg/buildings/infested_command_center.png"
		self.model = self.loadModel()
		self.model.setScale(8)
		self.model.setP(90)
		self.key = key
		self.originalHealth = 1000
		self.health = 1000
		self.model.setTag('key', key)
		self.alive = True
		
		self.collisionSolid = CollisionSphere(0, 0, -0.3, 0.75)
		self.collisionNode = CollisionNode('cnode_' + self.key)
		self.cnodePath = self.model.attachNewNode(self.collisionNode)
		self.cnodePath.node().addSolid(self.collisionSolid) 
		self.collisionNode.setIntoCollideMask(BitMask32(0x02))
		 

class AnimatedObject3D(VisualObject3D):
	'''
		This object will extended for buildings and other objects
		that are placed within the scene
		
		It represents the minerals in the game
	'''
	def __init__(self, parent, dummy, key):
		from object_const import ObjectConstants
		self.const = ObjectConstants()
		self.textureFile = "models/zerg/characters/zergling/zergling_0.png"
		
		self.super(parent, dummy, key, 2, self.const.ZERGLING, True, 1, Vec3(0, 0, -0.2), 0.6, 50, 5, 0.7)
		
		self.movies = dict()
		self.movies['walk'] = self.loadTextureMovie(8, "models/zerg/characters/zergling/zergling", "png")
		self.currentAnimation = ''
		self.fps = 7
		
	def super(self, parent, dummy, key, scale, type, animations, height, collision_pos, radius, health, damage, attackRadius):
		self.parent = parent
		self.dummy = dummy
		self.type = type
		self.model = self.loadModel()
		self.model.setScale(scale)
		self.model.setP(90)
		self.key = key
		self.model.setTag('key', key)
		self.height = height
		self.health = health
		self.originalHealth = health
		self.alive = True
		self.hasAnimation = animations
		self.currentSequence = None
		self.collisionPos = collision_pos
		self.radius = radius
		self.collisionNode = None
		self.attackRadius = attackRadius
		self.target = None
		self.damage = damage
		self.attacking = False
		self.attackTaskGo = False
	
	'''
		Makes this character die
	'''
	def dieAnimation(self):
		self.removeNode()
		self.singleplayer.removeItem(self.key)
		self.attackTaskGo = False
	
	'''
		Makes this character lose health points
	'''
	def loseHealth(self, hitPoints, attacker):
		self.health -= hitPoints
		if (self.health <= 0):
			self.alive = False
			if (self.dieAnimation):	
				self.dieAnimation()
		elif (not self.attackTaskGo):
			self.attack(attacker)
	
	'''
		Adds the collision detection to this unit
	'''
	def addCollisionDetection(self, traverser):
		self.collisionSolid = CollisionSphere(self.collisionPos[0], self.collisionPos[1], self.collisionPos[2], self.radius)
		self.collisionNode = CollisionNode('cnode_' + self.key)
		self.cnodePath = self.model.attachNewNode(self.collisionNode)
		self.collisionNode.addSolid(self.collisionSolid)
		self.collisionNode.setFromCollideMask(BitMask32(0x02))
		
		self.attackCollisionSolid = CollisionSphere(self.collisionPos[0], self.collisionPos[1], self.collisionPos[2], self.attackRadius)
		self.attackCollisionNode = CollisionNode('cnode_attack_' + self.key)
		self.attackCnodePath = self.model.attachNewNode(self.attackCollisionNode)
		self.attackCollisionNode.addSolid(self.attackCollisionSolid)
		self.attackCollisionNode.setFromCollideMask(BitMask32(0x02))
		self.attackCollisionNode.setIntoCollideMask(BitMask32(0x00))
		
		self.traverser = traverser
		self.pusher = CollisionHandlerPusher()
		self.traverser.addCollider(self.cnodePath, self.pusher)
		self.pusher.addCollider(self.cnodePath, self.model)
		
		self.collQueue = CollisionHandlerQueue()
		self.traverser.addCollider(self.attackCnodePath, self.collQueue)
		
	'''
		Creates an animation for the model
	'''
	def loadTextureMovie(self, frames, name, suffix, padding = 1):
		return [self.parent.loader.loadTexture((name+"_%0"+str(padding)+"d."+suffix) % i) for i in range(frames)]
	
	'''
		Starts the animation on the model 
	'''
	def startAnimation(self, key):
		if (self.hasAnimation):
			if (self.currentAnimation and self.currentAnimation != key):
				self.parent.taskMgr.remove(self.currentAnimation + '_' + self.key)
				
				self.currentAnimation = key
				self.animation = self.parent.taskMgr.add(self.animationTask, self.currentAnimation + '_' + self.key)
				self.prevTime = 0
				self.animation.justFired = False
			elif(not self.currentAnimation):
				self.currentAnimation = key
				self.animation = self.parent.taskMgr.add(self.animationTask, self.currentAnimation + '_' + self.key)
				self.prevTime = 0
				self.animation.justFired = False
	
		
	'''
		Ends the current animation
	'''
	def endAnimation(self):
		if (self.hasAnimation and self.currentAnimation):
			self.parent.taskMgr.remove(self.currentAnimation + '_' + self.key)
			if (self.alive):
				self.model.setTexture(self.movies[self.currentAnimation][0], 1)
				self.currentAnimation = ''
	
	'''
		Animation task for changing the current image
	'''
	def animationTask(self, task):
		if (self.alive):
			#Here we calculate the current frame number by multiplying the current time
			#(in seconds) by the frames per second variable we set earlier
			if (self.currentAnimation == 'attack'):
				fps = 0.75
			else:	
				fps = self.fps
			currentFrame = int(task.time * fps)
			delta_t = task.time - self.prevTime
			self.prevTime = task.time
			
			
			if (self.target and currentFrame % len(self.movies[self.currentAnimation]) == 1 and self.attacking and not task.justFired):
				self.target.loseHealth(self.damage, self)
				task.justFired = True
				if (self.type == self.const.MARINE):
					self.parent.soundLoader.playMusic('fire_gun')
			elif(currentFrame % len(self.movies[self.currentAnimation]) == 0):
				task.justFired = False
				
				
			
			self.model.setTexture(self.movies[self.currentAnimation][currentFrame % len(self.movies[self.currentAnimation])], 1)
			
			return task.cont
		else:
			return task.done
		
	'''
		Stops the current interval on the character
	'''
	def stop(self, attackTaskGo=False):
		if (self.currentSequence):
			self.currentSequence.pause()
		self.attackTaskGo = attackTaskGo
		
	'''
		Moves this character to the correct position using intervals
	'''
	def moveTo(self, pos, start=True):
		self.target = None
		self.stop()
		self.startAnimation('walk')
		dist = distance(self.getPos(), pos)
		speed = 10
		
		direction = pos - self.getPos()
		direction.normalize()
		heading = 0
		
		if (direction[0] > 0):
			heading = 1
		else:
			heading = -1
		
		scale = self.model.getTexScale(TextureStage.getDefault())
		self.model.setTexScale(TextureStage.getDefault(), math.fabs(scale[0])*heading, scale[1])
	
		self.currentSequence = Sequence(
			self.model.posInterval(dist/speed, pos),
			Func(self.endAnimation)
		)
		
		if (start):
			self.currentSequence.start()
		else:
			return (self.currentSequence, heading)
			
	
			
	'''
		Allows the unit to attack the indicated creature
		by moving towards him and attacking. If the other unit
		fleas this unit will, follow it until it kills it
		or it has been told to stop
	'''
	def attack(self, target):
		if (not self.attackTaskGo):
				attackTask = self.parent.taskMgr.add(self.attackTask, 'attacking_' + self.key)
				self.target = target
				attackTask.prevTime = 0
				self.attackTaskGo = True
		
	'''
		Makes this character follow the
		enemy to attack them
	'''
	def attackTask(self, task):
		deltaT = task.time - task.prevTime
		task.prevTime = task.time
		if (self.traverser and self.alive and self.target.alive and self.attackTaskGo):
			self.traverser.traverse(self.parent.render)
			self.collQueue.sortEntries()
			self.attacking = False
			for i in range(self.collQueue.getNumEntries()):
				cEntry = self.collQueue.getEntry(i)
				geomHit = cEntry.getIntoNodePath()
				key = geomHit.getParent().getTag('key')
				if (key == self.target.key):
					self.attacking = True
					break;
					
			if (self.attacking):
				self.stop(attackTaskGo=True)
				if (self.hasAnimation):
					if (self.movies.has_key('attack')):
						self.startAnimation('attack')
					else:
						self.startAnimation('walk')
			else:
				if (self.currentAnimation):
					self.endAnimation()
				self.startAnimation('walk')
				dist = distance(self.getPos(), self.target.getPos())
				
				direction = self.target.getPos() - self.getPos()
				direction.normalize()
				heading = 0
				
				if (direction[0] > 0):
					heading = 1
				else:
					heading = -1
				
				scale = self.model.getTexScale(TextureStage.getDefault())
				self.model.setTexScale(TextureStage.getDefault(), math.fabs(scale[0])*heading, scale[1])
				
				self.setX(self.getX() + 10*deltaT*direction[0])
				self.setZ(self.getZ() + 10*deltaT*direction[2])
				
			return task.cont
		else:
			if (self.currentAnimation):
					self.endAnimation()
			self.target = None
			return task.done
	
	'''
		Makes the texture face the correct direction
	'''
	def changeTextureDirection(self, heading):
		scale = self.model.getTexScale(TextureStage.getDefault())
		self.model.setTexScale(TextureStage.getDefault(), math.fabs(scale[0])*heading, scale[1])
	
class SCV(AnimatedObject3D):

	def __init__(self, parent, dummy, key):
		from object_const import ObjectConstants
		self.const = ObjectConstants()
		self.textureFile = "models/terran/characters/scv.png"
		
		self.super(parent, dummy, key, 3.5, self.const.SCV, False, 3.0, Vec3(0, 0, -0.2), 0.6, 50, 2, 0.7)
	
	'''
		Returns the two lists for adding buttons to the
		hud to control this unit
		
		This should not contain a move command however as that will
		be added by the singleplayer
	'''
	def getButtons(self):
		return [
			[
				'move', 'stop', 'attack',
				'', '', '',
				'building', '', ''
			],
			[
				'move', (self.stop, []), 'attack',
				'', '', '',
				'', '', ''
			]
		]
		
	'''
		Makes the scv attack another unit, but if that unit is a resource
		then it starts a continuous sequence to mine the resource
	'''
	def attack(self, target):
		objType = target.key.split('_')[0]
		if (objType == 'resource'):
			self.startMining(target)
			self.parent.taskMgr.remove('attacking_' + self.key)
		else:
			attackTask = self.parent.taskMgr.add(self.attackTask, 'attacking_' + self.key)
			attackTask.target = target
	
	
	'''
		Starts a sequence that handles the mining
		procedure for this scv given a target
		
		if the target is empty then the scv stops as if dead!
	'''
	def startMining(self, target):
		if (self.currentSequence):
			self.currentSequence.pause()
		
		diffVec = target.getPos() - self.commandCenter.getPos()
		diffVec.normalize()
		targetPos = target.getPos() - diffVec*0.75
		
		(firstSequence, firstHeading) = self.moveTo(targetPos, start=False)	
		(secondSequence, secondHeading) = self.moveTo(self.commandCenter.getPos(), start=False)
		
		self.currentSequence = Sequence(
			Func(self.changeTextureDirection, firstHeading),
			firstSequence,
			Wait(6),
			Func(self.show),
			Func(self.changeTextureDirection, secondHeading),
			secondSequence,
			Func(self.hide),
			Wait(6),
			Func(self.commandCenter.gameStats.addMinerals, 50),
			Func(self.show)
		)
		
		self.currentSequence.loop()
		
	'''
		Sets the comand center to return to
	'''
	def setCommandCenter(self, commandCenter):
		self.commandCenter = commandCenter
	
	'''
		Starts the scv building the structure
		Highlights the structure as gray while being built
	'''
	def startBuilding(self, target):
		target.setColor(0.5, 0.5, 0.5, 0.7)
		(moveToLocSeq, firstHeading) = self.moveTo(target.getPos(), start=False)
		diffVec = target.getPos() - Vec3(-3, 0, 5)
		(moveAwayFromLocSeq, secondHeading) = self.moveTo(diffVec, start=False)
		
		self.currentSequence = Sequence(
			Func(self.changeTextureDirection, firstHeading),
			moveToLocSeq,
			Func(self.hide),
			Wait(target.buildTime),
			Func(target.setColor, 1, 1, 1, 1),
			Func(target.allowClick, True),
			Func(self.show),
			Func(self.changeTextureDirection, secondHeading),
			moveAwayFromLocSeq
		)
		
		self.currentSequence.start()
		
		
	
		
class Marine(AnimatedObject3D):
	'''
		Creates an instance of a marine
	'''
	def __init__(self, parent, dummy, key):
		from object_const import ObjectConstants
		self.const = ObjectConstants()
		self.textureFile = "models/terran/characters/marine/marine_walk_0.png"
		#super(self, parent, dummy, key, scale, type, animations, height, collision_pos, radius, health, damage, attackRadius)
		self.super(parent, dummy, key, 3, self.const.MARINE, True, 2, Vec3(0, 0, -0.2), 0.6, 50, 10, 2.0)
		
		self.movies = dict()
		self.movies['walk'] = self.loadTextureMovie(9, "models/terran/characters/marine/marine_walk", "png")
		self.movies['attack'] = self.loadTextureMovie(2, "models/terran/characters/marine/marine_fire", "png")
		self.fps = 9
		self.currentAnimation = ''
		
		
	'''
		Returns the two lists for adding buttons to the
		hud to control this unit
		
		This should not contain a move command however as that will
		be added by the singleplayer
	'''
	def getButtons(self):
		return [
			[
				'move', 'stop', 'attack',
				'', '', '',
				'', '', ''
			],
			[
				'move', (self.stop, []), 'attack',
				'', '', '',
				'', '', ''
			]
		]
	